package com.ouyunc.oauth2.endpoint;


import com.ouyunc.common.constant.Auth2Constant;
import com.ouyunc.oauth2.service.IConsumerTokenServices;
import com.ouyunc.common.base.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import javax.servlet.http.HttpServletResponse;


/**
 * @author fangzhenxun
 * @date 2019/11/12 19:26
 * @description 用户登录/注销/刷新 api接口
 * @Description 用户名密码登录
 *      * 注意：如果第一次登录成功，那么在token有效期内的以后多次重复请求获取token会保证幂等性（即，access_token和refresh_token 相同），并且access_token和refresh_token有效期时间内，有效时间和第一次生成的时间是一致的
 *      * 如果解决在同一时间只有一个用户在线登录，有两个方法可以实现：
 *      * （1）可以在登录的前面先调用一下登出（注销接口）
 *      * （2）当然可以通过 IRedisTokenStore这个类对生成token规则进行修改，比如每次登陆都会把之前的token给删掉（无论到期不到期）然后重新生成token重新计时
 *      * extendMapJsonStr 放在请求头中的扩展json字符串map格式
 *      *
 *      *
 *      * 刷新token，一般情况下会将重复使用refresh_token的控制方法reuseRefreshTokens()设置为false，这样每次刷新的时候都会全部更新有效时间, 密码模式和认证授权模式走的是同一套流程
 *      *      * 注意：
 *      *      *      （1）当刷新token的时候如果在认证服务其设置重复使用refresh_token（即reuseRefreshTokens(true) 默认值）时 ；
 *      *      *          (1.1)在access_token没有过期时调用刷新token: ，
 *      *      *               生成的新access_token（有效时间会重新计时） 而老的old access_token(无论在不在有效期内)都不可以使用（已经被删掉了）
 *      *      *               生成新的refresh_token（有效时间不会重新计时），但是刷新后生成的新refresh_token 中的jti的值和之前老的的refresh_token 的jti的值是相同的（所以可以理解refresh_token没有改变，既可以重复使用原始的refresh_token），但是refresh_token字符串确实返回新的但是并没有在redis中存储，所以只能使用第一次（原始生成的refresh_token 来进行以后的刷新，如果使用后来生成的refresh_token来刷新则会报错）的refresh_token来继续以后的刷新，直到refresh_token过期
 *      *      *          (1.2)在access_token过期但refresh_token没有过期时调用刷新token:
 *      *      *              生成的新access_token（有效时间会重新计时）
 *      *      *              生成新的refresh_token（有效时间不会重新计时），但是刷新后生成的新refresh_token 中的jti的值和之前老的的refresh_token 的jti的值是相同的（所以可以理解refresh_token没有改变，既可以重复使用原始的refresh_token），但是refresh_token字符串确实返回新的但是并没有在redis中存储，所以只能使用第一次（原始生成的refresh_token 来进行以后的刷新，如果使用后来生成的refresh_token来刷新则会报错）的refresh_token来继续以后的刷新，直到refresh_token过期
 *      *      *          （1.3）在refresh_token过期的时候刷新，直接抛出异常
 *      *      *      （2）当刷新token的时候如果在认证服务其设置重复使用refresh_token（即reuseRefreshTokens(false）时 ；
 *      *      *          (2.1)在access_token没有过期时调用刷新token: ，
 *      *      *              生成的新access_token（有效时间会重新计时） 而老的old access_token(无论在不在有效期内)都不可以使用（已经被删掉了），
 *      *      *              生成的refresh_token （有效时间会重新计时） 而老的old access_token(无论在不在有效期内)都不可以使用（已经被删掉了）, 如果再次刷新则使用新生成的refresh_token
 *      *      *          (2.2)在access_token过期但refresh_token没有过期时调用刷新token:
 *      *      *              （2.2.1） TODO 这个问题需要解决：（默认情况下）如果先调用生成access_token接口，针对该用户会生成一套新的相关token（也包括新的刷新token）并且未失效的refresh_token还在生效，如果此时使用原来的老的refresh_token再次调用刷新token接口则会再次生成一套有效token，这个时候就会出现一个用户有两套有效的token可以使用，
 *      *      *                        解决方法：可以参考在调用生成token（登录）接口的时候先调用删除token接口（这里指的是删除刷新token的接口）
 *      *      *              （2.2.1）直接调用刷新token接口，则之前的refresh_token会被覆盖重新生成与计时
 *      *      *                  生成的新access_token（有效时间会重新计时）
 *      *      *                  生成的refresh_token （有效时间会重新计时） 而老的old access_token 已经被删除
 *      *      *          （2.3）在refresh_token过期的时候刷新，直接抛出异常
 */
@Api(tags = "登录/注销相关API")
@RestController
@RequestMapping("/oauth")
public class LoginEndpoint {
    private static Logger log = LoggerFactory.getLogger(LoginEndpoint.class);


    /**
     * oauth2 的客户端token操作
     **/
    @Autowired
    private IConsumerTokenServices iConsumerTokenServices;



    /**
     * @Author fangzhenxun
     * @Description  认证获取code, 重定向到oauth2 的认证，这样就不会出行session跨域失效的问题了
     * 注意：目前授权码认证的流程需要写在认证服务其中，牵涉到集群部署的问题
     * @Date 2020/4/24 15:58
     * @param clientId
     * @param redirectUri
     **/
    @ApiOperation(value = "授权码模式-获取code", response = ResponseResult.class)
    @ApiImplicitParams({@ApiImplicitParam(name = "clientId", value = "客户端id",dataTypeClass = String.class, required = true, paramType = "query"), @ApiImplicitParam(name = "redirectUri", value = "重定向地址",dataTypeClass = String.class, required = true, paramType = "query"), @ApiImplicitParam(name = "state", value = "标识常量，传递什么，返回什么", dataTypeClass = String.class,required = false, paramType = "query")})
    @GetMapping("/authentication/code")
    public void authentication(@RequestParam("clientId") String clientId, @RequestParam("redirectUri") String redirectUri, @RequestParam(value = "state", required = false) String state) throws Exception {
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getResponse();
        //认证的url
        UriComponents uri = UriComponentsBuilder.fromPath(Auth2Constant.OAUTH_AUTHORIZE_URL)
                .queryParam(Auth2Constant.CLIENT_ID, clientId)
                .queryParam(Auth2Constant.RESPONSE_TYPE, Auth2Constant.CODE)
                .queryParam(Auth2Constant.REDIRECT_URI, redirectUri)
                .queryParam(Auth2Constant.STATE, state)
                .encode().build();
        response.sendRedirect(uri.toString());
    }



    /**
     * @Author fangzhenxun
     * @Description   @Resource(name = "consumerTokenServices")
     *   private ConsumerTokenServices consumerTokenServices;
     * 授权码和用户名密码都可以用， 注销（登出）用户,注意在删除token的时候会有客户端的两个redis还保存着，分别是client_id_to_access和uname_to_access
     * 注意这里的路径 /logout 可能会与oauth有冲突，所以这里需要加一个前缀路径oauth2
     * 加上@RedisTokenPrefix 注解会全局拦截动态取出当前token 在redis中key的前缀
     * @Date 2020/4/20 13:04
     * @param accessToken
     * @return com.xyt.common.entity.base.XytResponse
     **/
    @ApiOperation(value = "密码模式-退出登录", response = ResponseResult.class)
    @ApiImplicitParams({@ApiImplicitParam(name = "accessToken", value = "token",dataTypeClass = String.class, required = true, paramType = "query"), @ApiImplicitParam(name = "extendMap",dataTypeClass = String.class, value = "请求头扩展信息==>根据业务自定义json字符串", required = true, paramType = "header", defaultValue = "{\"phone\":\"15656589658\",\"userType\":\"user\"}")})
    @PostMapping("/logout")
    public ResponseResult logout(@RequestParam("accessToken") String accessToken, @RequestHeader(value = "extendMap",required = false) String extendMapJsonStr) {
        return ResponseResult.success(iConsumerTokenServices.revokeToken(accessToken));
    }


}
